// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/renderer/media/renderer_webmediaplayer_delegate.h"

#include <stdint.h>

#include "base/auto_reset.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics_action.h"
#include "base/sys_info.h"
#include "content/common/media/media_player_delegate_messages.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "third_party/WebKit/public/platform/WebMediaPlayer.h"
#include "third_party/WebKit/public/web/WebScopedUserGesture.h"

#if defined(OS_ANDROID)
#include "base/android/build_info.h"
#endif

namespace {

void RecordAction(const base::UserMetricsAction& action)
{
    content::RenderThread::Get()->RecordAction(action);
}

} // namespace

namespace media {

RendererWebMediaPlayerDelegate::RendererWebMediaPlayerDelegate(
    content::RenderFrame* render_frame)
    : RenderFrameObserver(render_frame)
    , default_tick_clock_(new base::DefaultTickClock())
    , tick_clock_(default_tick_clock_.get())
{
    idle_cleanup_interval_ = base::TimeDelta::FromSeconds(5);
    idle_timeout_ = base::TimeDelta::FromSeconds(15);

    // Idle players time out more aggressively on low end devices.
    is_low_end_device_ = base::SysInfo::IsLowEndDevice();

#if defined(OS_ANDROID)
    // On Android, due to the instability of the OS level media components, we
    // consider all pre-KitKat devices to be low end.
    is_low_end_device_ |= base::android::BuildInfo::GetInstance()->sdk_int() <= 18;
#endif
}

RendererWebMediaPlayerDelegate::~RendererWebMediaPlayerDelegate() { }

bool RendererWebMediaPlayerDelegate::IsFrameHidden()
{
    if (is_frame_hidden_for_testing_)
        return true;

    return (render_frame() && render_frame()->IsHidden()) || is_frame_closed_;
}

bool RendererWebMediaPlayerDelegate::IsFrameClosed()
{
    return is_frame_closed_;
}

int RendererWebMediaPlayerDelegate::AddObserver(Observer* observer)
{
    return id_map_.Add(observer);
}

void RendererWebMediaPlayerDelegate::RemoveObserver(int player_id)
{
    DCHECK(id_map_.Lookup(player_id));
    id_map_.Remove(player_id);
    idle_player_map_.erase(player_id);
    stale_players_.erase(player_id);
    playing_videos_.erase(player_id);

    Send(
        new MediaPlayerDelegateHostMsg_OnMediaDestroyed(routing_id(), player_id));

    ScheduleUpdateTask();
}

void RendererWebMediaPlayerDelegate::DidPlay(
    int player_id,
    bool has_video,
    bool has_audio,
    MediaContentType media_content_type)
{
    DVLOG(2) << __func__ << "(" << player_id << ", " << has_video << ", "
             << has_audio << ", " << static_cast<int>(media_content_type) << ")";
    DCHECK(id_map_.Lookup(player_id));

    has_played_media_ = true;
    if (has_video) {
        if (!playing_videos_.count(player_id)) {
            playing_videos_.insert(player_id);
            has_played_video_ = true;
        }
    } else {
        playing_videos_.erase(player_id);
    }

    Send(new MediaPlayerDelegateHostMsg_OnMediaPlaying(
        routing_id(), player_id, has_video, has_audio, false,
        media_content_type));

    ScheduleUpdateTask();
}

void RendererWebMediaPlayerDelegate::DidPause(int player_id)
{
    DVLOG(2) << __func__ << "(" << player_id << ")";
    DCHECK(id_map_.Lookup(player_id));
    playing_videos_.erase(player_id);
    Send(new MediaPlayerDelegateHostMsg_OnMediaPaused(routing_id(), player_id,
        false));

    // Required to keep background playback statistics up to date.
    ScheduleUpdateTask();
}

void RendererWebMediaPlayerDelegate::PlayerGone(int player_id)
{
    DVLOG(2) << __func__ << "(" << player_id << ")";
    DCHECK(id_map_.Lookup(player_id));
    playing_videos_.erase(player_id);
    Send(
        new MediaPlayerDelegateHostMsg_OnMediaDestroyed(routing_id(), player_id));

    // Required to keep background playback statistics up to date.
    ScheduleUpdateTask();
}

void RendererWebMediaPlayerDelegate::SetIdle(int player_id, bool is_idle)
{
    DVLOG(2) << __func__ << "(" << player_id << ", " << is_idle << ")";

    if (is_idle == IsIdle(player_id))
        return;

    if (is_idle) {
        idle_player_map_[player_id] = tick_clock_->NowTicks();
    } else {
        idle_player_map_.erase(player_id);
        stale_players_.erase(player_id);
    }

    ScheduleUpdateTask();
}

bool RendererWebMediaPlayerDelegate::IsIdle(int player_id)
{
    return idle_player_map_.count(player_id) || stale_players_.count(player_id);
}

void RendererWebMediaPlayerDelegate::ClearStaleFlag(int player_id)
{
    DVLOG(2) << __func__ << "(" << player_id << ")";

    if (!stale_players_.erase(player_id))
        return;

    // Set the idle time such that the player will be considered stale the next
    // time idle cleanup runs.
    idle_player_map_[player_id] = tick_clock_->NowTicks() - idle_timeout_;

    ScheduleUpdateTask();
}

bool RendererWebMediaPlayerDelegate::IsStale(int player_id)
{
    return stale_players_.count(player_id);
}

void RendererWebMediaPlayerDelegate::WasHidden()
{
    RecordAction(base::UserMetricsAction("Media.Hidden"));

    for (IDMap<Observer*>::iterator it(&id_map_); !it.IsAtEnd(); it.Advance())
        it.GetCurrentValue()->OnFrameHidden();

    ScheduleUpdateTask();
}

void RendererWebMediaPlayerDelegate::WasShown()
{
    RecordAction(base::UserMetricsAction("Media.Shown"));
    is_frame_closed_ = false;

    for (IDMap<Observer*>::iterator it(&id_map_); !it.IsAtEnd(); it.Advance())
        it.GetCurrentValue()->OnFrameShown();

    ScheduleUpdateTask();
}

bool RendererWebMediaPlayerDelegate::OnMessageReceived(
    const IPC::Message& msg)
{
    IPC_BEGIN_MESSAGE_MAP(RendererWebMediaPlayerDelegate, msg)
    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_Pause, OnMediaDelegatePause)
    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_Play, OnMediaDelegatePlay)
    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_SuspendAllMediaPlayers,
        OnMediaDelegateSuspendAllMediaPlayers)
    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_UpdateVolumeMultiplier,
        OnMediaDelegateVolumeMultiplierUpdate)
    IPC_MESSAGE_UNHANDLED(return false)
    IPC_END_MESSAGE_MAP()
    return true;
}

void RendererWebMediaPlayerDelegate::SetIdleCleanupParamsForTesting(
    base::TimeDelta idle_timeout,
    base::TickClock* tick_clock,
    bool is_low_end_device)
{
    idle_cleanup_interval_ = base::TimeDelta();
    idle_timeout_ = idle_timeout;
    tick_clock_ = tick_clock;
    is_low_end_device_ = is_low_end_device;
}

bool RendererWebMediaPlayerDelegate::IsIdleCleanupTimerRunningForTesting()
    const
{
    return idle_cleanup_timer_.IsRunning();
}

void RendererWebMediaPlayerDelegate::SetFrameHiddenForTesting(bool is_hidden)
{
    if (is_hidden == is_frame_hidden_for_testing_)
        return;

    is_frame_hidden_for_testing_ = is_hidden;

    ScheduleUpdateTask();
}

void RendererWebMediaPlayerDelegate::OnMediaDelegatePause(int player_id)
{
    RecordAction(base::UserMetricsAction("Media.Controls.RemotePause"));

    Observer* observer = id_map_.Lookup(player_id);
    if (observer) {
        // TODO(avayvod): remove when default play/pause is handled via
        // the MediaSession code path.
        std::unique_ptr<blink::WebScopedUserGesture> gesture(
            render_frame()
                ? new blink::WebScopedUserGesture(render_frame()->GetWebFrame())
                : nullptr);
        observer->OnPause();
    }
}

void RendererWebMediaPlayerDelegate::OnMediaDelegatePlay(int player_id)
{
    RecordAction(base::UserMetricsAction("Media.Controls.RemotePlay"));

    Observer* observer = id_map_.Lookup(player_id);
    if (observer) {
        // TODO(avayvod): remove when default play/pause is handled via
        // the MediaSession code path.
        std::unique_ptr<blink::WebScopedUserGesture> gesture(
            render_frame()
                ? new blink::WebScopedUserGesture(render_frame()->GetWebFrame())
                : nullptr);
        observer->OnPlay();
    }
}

void RendererWebMediaPlayerDelegate::OnMediaDelegateSuspendAllMediaPlayers()
{
    is_frame_closed_ = true;

    for (IDMap<Observer*>::iterator it(&id_map_); !it.IsAtEnd(); it.Advance())
        it.GetCurrentValue()->OnFrameClosed();
}

void RendererWebMediaPlayerDelegate::OnMediaDelegateVolumeMultiplierUpdate(
    int player_id,
    double multiplier)
{
    Observer* observer = id_map_.Lookup(player_id);
    if (observer)
        observer->OnVolumeMultiplierUpdate(multiplier);
}

void RendererWebMediaPlayerDelegate::ScheduleUpdateTask()
{
    if (!pending_update_task_) {
        base::ThreadTaskRunnerHandle::Get()->PostTask(
            FROM_HERE,
            base::Bind(&RendererWebMediaPlayerDelegate::UpdateTask, AsWeakPtr()));
        pending_update_task_ = true;
    }
}

void RendererWebMediaPlayerDelegate::UpdateTask()
{
    DVLOG(3) << __func__;
    pending_update_task_ = false;

    // Check whether a player was played since the last UpdateTask(). We basically
    // treat this as a parameter to UpdateTask(), except that it can be changed
    // between posting the task and UpdateTask() executing.
    bool has_played_video_since_last_update_task = has_played_video_;
    has_played_video_ = false;

    // Record UMAs for background video playback.
    RecordBackgroundVideoPlayback();

    // Clean up idle players.
    bool aggressive_cleanup = false;

    // When we reach the maximum number of idle players, clean them up
    // aggressively. Values chosen after testing on a Galaxy Nexus device for
    // http://crbug.com/612909.
    if (idle_player_map_.size() > (is_low_end_device_ ? 2u : 8u))
        aggressive_cleanup = true;

    // When a player plays on a low-end device, clean up idle players
    // aggressively.
    if (has_played_video_since_last_update_task && is_low_end_device_)
        aggressive_cleanup = true;

    CleanUpIdlePlayers(aggressive_cleanup ? base::TimeDelta() : idle_timeout_);

    // If there are still idle players, schedule an attempt to clean them up.
    // This construct ensures that the next callback is always
    // |idle_cleanup_interval_| from now.
    idle_cleanup_timer_.Stop();
    if (!idle_player_map_.empty()) {
        idle_cleanup_timer_.Start(
            FROM_HERE, idle_cleanup_interval_,
            base::Bind(&RendererWebMediaPlayerDelegate::UpdateTask,
                base::Unretained(this)));
    }
}

void RendererWebMediaPlayerDelegate::RecordBackgroundVideoPlayback()
{
#if defined(OS_ANDROID)
    // TODO(avayvod): This would be useful to collect on desktop too and express
    // in actual media watch time vs. just elapsed time.
    // See https://crbug.com/638726.
    bool has_playing_background_video = IsFrameHidden() && !IsFrameClosed() && !playing_videos_.empty();

    if (has_playing_background_video != was_playing_background_video_) {
        was_playing_background_video_ = has_playing_background_video;

        if (has_playing_background_video) {
            RecordAction(base::UserMetricsAction("Media.Session.BackgroundResume"));
            background_video_start_time_ = base::TimeTicks::Now();
        } else {
            RecordAction(base::UserMetricsAction("Media.Session.BackgroundSuspend"));
            UMA_HISTOGRAM_CUSTOM_TIMES(
                "Media.Android.BackgroundVideoTime",
                base::TimeTicks::Now() - background_video_start_time_,
                base::TimeDelta::FromSeconds(7), base::TimeDelta::FromHours(10), 50);
        }
    }
#endif // OS_ANDROID
}

void RendererWebMediaPlayerDelegate::CleanUpIdlePlayers(
    base::TimeDelta timeout)
{
    const base::TimeTicks now = tick_clock_->NowTicks();

    // Create a list of stale players before making any possibly reentrant calls
    // to OnIdleTimeout().
    std::vector<int> stale_players;
    for (const auto& it : idle_player_map_) {
        if (now - it.second >= timeout)
            stale_players.push_back(it.first);
    }

    // Notify stale players.
    for (int player_id : stale_players) {
        Observer* player = id_map_.Lookup(player_id);
        if (player && idle_player_map_.erase(player_id)) {
            stale_players_.insert(player_id);
            player->OnIdleTimeout();
        }
    }
}

void RendererWebMediaPlayerDelegate::OnDestruct()
{
    delete this;
}

} // namespace media
